Skip to content

CR-133: Report terminal revocation as healthy-as-revoked (implementation slice) - #178

Merged
coreytshaffer merged 2 commits into
mainfrom
claude/cr-133-revoked-identity-health-implementation
Aug 15, 2026
Merged

CR-133: Report terminal revocation as healthy-as-revoked (implementation slice)#178
coreytshaffer merged 2 commits into
mainfrom
claude/cr-133-revoked-identity-health-implementation

Conversation

@coreytshaffer

Copy link
Copy Markdown
Owner

Implements the CR-133 accepted option (a) semantics under the single-slice
implementation-authority grant recorded on 2026-08-14, bounded to the four-file
allowlist.

LifecycleHealthy != OperationallyUsable != CapabilityReady

Recorded pre-change behavior

Measured read-only against main@770d9f2 for a revoked identity:

$ tc identity doctor agent-001          # exit 1
ERROR   no_active_key
WARNING missing_rotated_at
WARNING missing_archived_key

The two warnings were structurally unreachable as true: revoke_identity()
neither stamps rotated_at nor archives key material, so a revoked record was
being measured against rotation-shaped expectations it can never satisfy.

Why both surfaces change together

A change confined to check_health() would have introduced a worse defect. The
--for-capability loop skips any agent without exactly one active identity, and
the process exits non-zero only when the report has errors. Today's non-zero exit
for a revoked agent comes entirely from no_active_key. Removing it alone makes
tc identity doctor <revoked> --for-capability X exit 0 in silence — emitting
neither a capability error nor a readiness line — reporting success for an identity
the accepted semantics state is not capability-ready.

Approach

A shared module-level predicate, is_terminal_revoked(), classifies the accepted
state narrowly — zero ACTIVE_STATUS, exactly one REVOKED_STATUS, every
remaining record ROTATED_STATUS. Both check_health() and tc_identity_doctor()
consume it, so the two surfaces cannot drift apart on the classification.

Reachability of the refused shapes was probed rather than assumed: multiply-revoked
is unreachable through supported commands (identity init refuses an existing
agent_id), and COMPROMISED_STATUS has no production writer. Both are hand-edit
only, and both keep their current findings.

--for-capability fails via a distinct revoked_identity_not_capability_ready
code. Reusing missing_requested_capability would assert the capability is absent
when revocation is the operative reason — a different and often false fact. The
message claims nothing about whether the capability was ever granted, so it holds
for both the granted and never-granted cases.

The revoked state is stated positively rather than merely non-erroring, so a bare
Identity doctor passed cannot be misread as "this signer is ready":

OK lifecycle_state=revoked agent_id=agent-001 fingerprint=<fp> operationally_usable=false capability_ready=false

Constraints preserved

  • ROTATED_STATUS historical-integrity diagnostics intact — proven by a test that
    deletes the archive on a rotated-then-revoked agent and asserts the warning still
    fires against the rotated fingerprint and not the revoked one
  • COMPROMISED_STATUS health behavior unchanged through both the zero-active and
    historical-record paths. The historical guard stays a negative exemption rather
    than being rewritten to status == ROTATED_STATUS, and suppression is conditioned
    on terminal revocation rather than on "no active identity" — the two vectors
    CR-133 names
  • no_active_key intact for every other zero-active cause
  • Revoked identities remain unusable for signing, verification, authorization, and
    capability readiness — verify_signed_payload() and
    require_authorized_capability() untouched
  • IdentityDoctorReport, IdentityDoctorIssue, revoke_identity(), and
    check_consistency() unchanged
  • No private-key retention, archival, or deletion policy introduced or inferred

Verification

Full suite: 1718 passed, 6 skipped. Nine new cases in tests/test_doctor_cli.py
cover the CR's four required regression cases plus a compromised-state behavioral
non-change proof, a parametrized guard against widening the predicate, a read-only
guarantee on the revoked path, and narrow check/doctor non-contradiction for the
revoked state only.

Scope

Independent of PR #177, which adds only the CR document — no file overlap. This
branch is based at origin/main@770d9f2.

The grant covers preparation of this reviewable candidate only. It does not carry
implementation acceptance, merge, release, or closeout authority. The four deferred
archived-design coverage gaps remain outside this slice.

🤖 Generated with Claude Code

Implements the CR-133 accepted option (a) semantics:

    LifecycleHealthy != OperationallyUsable != CapabilityReady

At main@770d9f2 a revoked identity produced `no_active_key` plus
`missing_rotated_at` and `missing_archived_key`, conflating a valid terminal
lifecycle state with a fault. The two warnings were structurally unreachable
as true, since revocation neither stamps rotated_at nor archives key material.

Suppressing those findings alone would have introduced a worse defect: the
`--for-capability` loop skips agents without exactly one active identity and
the process exits non-zero only on report errors, so the invocation would have
exited 0 in silence, reporting success for an identity that is not capability
ready. Both surfaces are therefore changed together.

A shared module-level predicate classifies the accepted state narrowly -- zero
active, exactly one revoked, every remaining record rotated -- so check_health()
and tc_identity_doctor() cannot drift apart on the classification.

Preserved: ROTATED_STATUS archival diagnostics; COMPROMISED_STATUS health
behavior through both the zero-active and historical-record paths;
`no_active_key` for every other zero-active cause; revoked identities remain
unusable for signing, verification, authorization, and capability readiness;
IdentityDoctorReport and IdentityDoctorIssue unchanged; no private-key
disposition introduced or inferred.

`--for-capability` fails via a distinct `revoked_identity_not_capability_ready`
code rather than reusing `missing_requested_capability`, which would assert
that the capability is absent when revocation is the operative reason.

Scope: the four-file allowlist recorded in CR-133. Full suite 1718 passed,
6 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 14, 2026

Copy link
Copy Markdown

Deploy Preview for poetic-quokka-0fd859 ready!

Name Link
🔨 Latest commit bbb36d4
🔍 Latest deploy log https://app.netlify.com/projects/poetic-quokka-0fd859/deploys/6a7fa3e82b713100084fcd08
😎 Deploy Preview https://deploy-preview-178--poetic-quokka-0fd859.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

The regression suite asserted printed status but not the process exit code:
run_cli_command() swallowed SystemExit and returned stdout only. CR-133's
post-change-state trap is specifically an exit-code defect -- a revoked identity
exiting 0 while emitting no capability finding -- so the contract that matters
most was unpinned.

Adds run_cli_command_with_exit_code() returning (stdout, exit_code); the
existing helper delegates to it, so no pre-existing test changes shape. main()
returning without raising is treated as exit 0.

Pinned:

    doctor <terminal-revoked>                    -> 0
    doctor <terminal-revoked> --for-capability   -> 1
    doctor <active, capability present>          -> 0   (positive control)
    doctor <active, capability absent>           -> 1   (negative control)

Verified load-bearing by mutation: changing only the doctor's terminal
sys.exit(1) to sys.exit(0), leaving all printed output byte-identical, fails
exactly the three exit-code-pinned failure cases and nothing else.

Full suite 1718 passed, 6 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coreytshaffer added a commit that referenced this pull request Aug 14, 2026
…nd exhausted

The CR text still read "Implementation authority: Still withheld", which no
longer matched the governance state: a bounded single-slice grant was made by
the human operator on 2026-08-14 and exercised as PR #178. The durable record
had not caught up with the decision.

Records the grant verbatim as its own section -- four-file allowlist, the shared
terminal-revoked helper explicitly in scope, the narrowly bounded predicate, the
permitted changes, and the preserved constraints -- then notes that the grant is
exhausted now that the reviewable candidate exists, and that implementation
acceptance was reviewed and withheld pending two evidence repairs.

Also repoints statements the grant would otherwise contradict: Scope, Human
Approval Requirement, Required Regression Set, Explicit Exclusions, Deferred
Work, and Stop Point, which now stops a fourth time before acceptance rather
than a third time before implementation. The deferred archived-design coverage
gaps are stated as still outside the grant.

Recorded on the CR branch rather than folded into PR #178, and without
rewriting a5ed999.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coreytshaffer added a commit that referenced this pull request Aug 14, 2026
…bb36d4

Implementation acceptance was granted by the human operator on 2026-08-14,
following implementation-design review, code review, and two evidence repairs
(the authority record at f952e26, the exit-code pins at bbb36d4). Acceptance
attaches to that specific revision and settles correctness, not landing.

Repoints the passages the grant makes stale: the acceptance withholding is
restated as first-review history with its resolution, and the Stop Point now
stops a fifth time before merge authority rather than a fourth time before
acceptance.

Records the recommended merge ordering -- #177 first, then #178 -- with its
rationale. The two PRs have no file overlap, so nothing forces that order, which
is why it is written down: landing the authority and acceptance record before the
runtime change it authorized gives repository history the same causal order as
the governance process.

Merge, release, and closeout authority remain ungranted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coreytshaffer
coreytshaffer merged commit 8b5d330 into main Aug 15, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant